﻿using System;
using System.Collections.Generic;
using ParserGenerator.Extensions;
using ParserGenerator.GrammarConstruction;
using ParserGenerator.Output;

namespace ParserGenerator.ParserConstruction
{
    [Serializable]
    public sealed class ParserAutomatonGraphvizWriter
    {
        public IndentedWriter Writer { get; }

        public ParserAutomatonGraphvizWriter(IndentedWriter writer)
        {
            if (writer == null)
                throw new ArgumentNullException(nameof(writer));
            Writer = writer;
        }

        private void WriteGraphvizHeader()
        {
            Writer.Write("digraph G\n");
            Writer.Write("{\n");
            Writer.OpenBlock();
            Writer.Write("rankdir = LR;\n");
            Writer.Write("graph [ dpi = 300 ];\n");
        }

        private void WriteGraphvizBody(ParserAutomaton automaton)
        {
            if (automaton == null)
                throw new ArgumentNullException(nameof(automaton));
            WriteParserAutomatonStates(automaton);
            WriteParserAutomatonLinks(automaton);
            Writer.CloseBlock();
            Writer.Write("}");
        }

        private void WriteParserAutomatonStates(ParserAutomaton automaton)
        {
            if (automaton == null)
                throw new ArgumentNullException(nameof(automaton));
            foreach (GrammarGoalDescriptor goal in automaton.Goals.Values)
                WriteParserAugomatonGoal(goal);
            foreach (ParserAutomatonState state in automaton.States)
                WriteParserAutomatonState(state);
        }

        private void WriteParserAugomatonGoal(GrammarGoalDescriptor goal)
        {
            if (goal == null)
                throw new ArgumentNullException(nameof(goal));
            Writer.Write("\n{0}\n", ~goal.Number);
            Writer.Write("[\n");
            Writer.OpenBlock();
            Writer.Write("shape = ellipse\n");
            Writer.Write("label = <");
            WriteGrammarSymbol(goal.GoalSymbol);
            Writer.Write(">\n");
            Writer.CloseBlock();
            Writer.Write("];\n");
        }

        private void WriteParserAutomatonState(ParserAutomatonState state)
        {
            if (state == null)
                throw new ArgumentNullException(nameof(state));
            Writer.Write("\n{0}\n", state.Number);
            Writer.Write("[\n");
            Writer.OpenBlock();
            Writer.Write("shape = none\n");
            Writer.Write("label = <<table border=\"0\" cellspacing=\"0\">\n");
            Writer.OpenBlock();
            Writer.OpenBlock();
            Writer.OpenBlock();
            Writer.Write("<tr><td align=\"left\">{0}</td></tr>\n", state.Number);
            HashSet<GrammarTerminalSymbol> conflictedTokens = state.GetConflictedTokens();
            WriteItemSet(state, state.Core, conflictedTokens);
            ParserAutomatonItemSet closure = state.Core.ComputeClosure();
            WriteItemSet(state, closure, conflictedTokens, "lightgray");
            Writer.CloseBlock();
            Writer.Write("</table>>\n");
            Writer.CloseBlock();
            Writer.CloseBlock();
            Writer.CloseBlock();
            Writer.Write("];\n");
        }

        private void WriteGrammarSymbol(GrammarSymbol symbol)
        {
            if (symbol == null)
                throw new ArgumentNullException(nameof(symbol));
            Writer.Write("<i>{0}</i>", symbol.Name.AsHtmlText());
        }

        private void WriteItem(ParserAutomatonItem item)
        {
            if (item == null)
                throw new ArgumentNullException(nameof(item));
            WriteGrammarSymbol(item.Production.Owner);
            Writer.Write(" &rarr;");
            for (int i = 0; i <= item.Production.Length; i++)
            {
                if (i == item.DotPosition)
                    Writer.Write(" .");
                if (i < item.Production.Length)
                {
                    Writer.Write(" ");
                    WriteGrammarSymbol(item.Production.Symbols[i]);
                }
            }
        }

        private void WriteLookaheadSet(ParserAutomatonReductionLookaheadSet lookaheadSet, HashSet<GrammarTerminalSymbol> conflictedTokens)
        {
            if (lookaheadSet == null)
                throw new ArgumentNullException(nameof(lookaheadSet));
            if (conflictedTokens == null)
                throw new ArgumentNullException(nameof(conflictedTokens));
            Writer.Write(" {");
            int k = 0;
            foreach (GrammarTerminalSymbol sym in lookaheadSet)
            {
                if (k++ > 0)
                    Writer.Write(", ");
                string color = conflictedTokens.Contains(sym) ? "red" : "blue";
                WriteLookaheadSymbol(sym, color);
            }
            Writer.Write("}");
        }

        private void WriteLookaheadSymbol(GrammarTerminalSymbol token, string color)
        {
            if (token == null)
                throw new ArgumentNullException(nameof(token));
            if (color == null)
                throw new ArgumentNullException(nameof(color));
            Writer.Write("<font color=\"{0}\">", color);
            WriteGrammarSymbol(token);
            Writer.Write("</font>");
        }

        private void WriteItemSet(ParserAutomatonState state, ParserAutomatonItemSet items, HashSet<GrammarTerminalSymbol> conflictedTokens)
        {
            if (state == null)
                throw new ArgumentNullException(nameof(state));
            if (items == null)
                throw new ArgumentNullException(nameof(items));
            if (conflictedTokens == null)
                throw new ArgumentNullException(nameof(conflictedTokens));
            WriteItemSet(state, items, conflictedTokens, null);
        }

        private void WriteItemSet(ParserAutomatonState state, ParserAutomatonItemSet items, HashSet<GrammarTerminalSymbol> conflictedTokens, string color)
        {
            if (state == null)
                throw new ArgumentNullException(nameof(state));
            if (items == null)
                throw new ArgumentNullException(nameof(items));
            if (conflictedTokens == null)
                throw new ArgumentNullException(nameof(conflictedTokens));
            foreach (ParserAutomatonItem item in items)
            {
                Writer.Write("<tr><td border=\"1\"");
                if (color != null)
                    Writer.Write(" bgcolor=\"{0}\"", color);
                Writer.Write(">");
                WriteItem(item);
                if (!state.IsConsistent && item.IsReduce)
                {
                    ParserAutomatonReduction reduce = state.GetReduction(item.Production);
                    ParserAutomatonReductionLookaheadSet lookaheadSet = reduce.LookaheadSet;
                    WriteLookaheadSet(lookaheadSet, conflictedTokens);
                }
                Writer.Write("</td></tr>\n");
            }
        }

        private void WriteParserAutomatonLinks(ParserAutomaton automaton)
        {
            if (automaton == null)
                throw new ArgumentNullException(nameof(automaton));
            Writer.Write("\n");
            foreach (GrammarGoalDescriptor goal in automaton.Goals.Values)
            {
                Writer.Write("{0} -> {1} [ label = <", ~goal.Number, goal.State.Number);
                WriteGrammarSymbol(goal.StartSymbol);
                Writer.Write(">; color = brown ];\n");
            }
            foreach (ParserAutomatonState state in automaton.States)
            {
                foreach (var t in state.Transitions)
                {
                    Writer.Write("{0} -> {1} [ label = <", state, t.Value.Target);
                    WriteGrammarSymbol(t.Key);
                    Writer.Write("> ];\n");
                }
            }
        }

        public void WriteParserAutomaton(ParserAutomaton automaton)
        {
            if (automaton == null)
                throw new ArgumentNullException(nameof(automaton));
            WriteGraphvizHeader();
            WriteGraphvizBody(automaton);
        }
    }
}
